/*
 * Copyright (C) 2011 Andreas Petersson
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package at.petersson.interner;

import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;

import java.lang.reflect.Field;
import java.util.*;


/**
 * @author apetersson
 */
public class DeepInterner {

    private final ImmutabilityChecker immutabilityChecker;

    private final Map<Object, Object> weakReferernces = Collections.synchronizedMap(new WeakHashMap<Object, Object>());

    public DeepInterner(ImmutabilityChecker immutabilityChecker) {
        this.immutabilityChecker = immutabilityChecker;
    }

    @SuppressWarnings("unchecked")
    public <T> T intern(T object) {
        String errorMsg = immutabilityChecker.isImmutable(object.getClass());
        Preconditions.checkArgument(errorMsg == null, errorMsg);
        try {
            return (T) intern2(object, new IdentityHashMap<Object, List<Notifier>>());
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }


    Object intern2(final Object notInterned, IdentityHashMap<Object, List<Notifier>> workList) throws IllegalAccessException {
        List<Notifier> notifiers = Lists.newArrayList();
        workList.put(notInterned, notifiers);
        Iterable<Field> fields = Iterables.filter(ImmutabilityChecker.nonstaticFields(notInterned.getClass()), new Predicate<Field>() {
            @Override
            public boolean apply(Field input) {
                return !input.getType().isPrimitive() && !input.isEnumConstant();
            }
        });
        for (final Field field : fields) {
            field.setAccessible(true);
            Object fieldValue = field.get(notInterned);
            final List<Notifier> runnable = workList.get(fieldValue);
            if (runnable == null) {
                Object interned = (fieldValue == null) ? null : intern2(fieldValue, workList);
                field.set(notInterned, interned);
            } else {//do not recurse, it is already being worked on... instead notify me as soon as a result is available
                runnable.add(new Notifier() {
                    @Override
                    public void run(Object freshlyInterned) throws IllegalAccessException {
                        field.set(notInterned, freshlyInterned);
                    }
                });
            }
        }
        final Object wrapped;
        if (notInterned.getClass().isArray()) {
            wrapped = Wrappers.replaceWithWrapper(notInterned, this, workList);
        } else {
            wrapped = notInterned;
        }
        Object interned = simpleIntern(wrapped);
        for (Notifier notifier : notifiers) {
            notifier.run(interned);
        }
        return interned;
    }

    private Object simpleIntern(Object wrapped) {
        final Object cached = weakReferernces.get(wrapped);
        final Object ret;
        if (cached != null) {
            ret = cached;
        } else {
            weakReferernces.put(wrapped, wrapped);
            ret = wrapped;
        }
        if (ret instanceof Wrapper) {
            return ((Wrapper) ret).unwrap();
        } else {
            return ret;
        }
    }

}
